/**
 * Timed Typing Tester
 * By: Justin Sturm
 * For: Vitera Healthcare Solutions
 * 
 * Resources Used:
 * 		Eclipse IDE (http://www.eclipse.org)
 * 		WindowBuilder (Eclipse Plugin; http://www.eclipse.org/windowbuilder/)
 * 		JavaDocs (http://docs.oracle.com/javase/6/docs/api/overview-summary.html)
 */

import java.awt.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

import java.awt.event.*;
import java.text.DecimalFormat;

import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.border.BevelBorder;

/** 
 * TTTFrame (short for Timed Typing Test Frame) extends the JFrame class with methods
 * useful specifically to this typing challenge project.  The main method is at the very bottom
 * and creates a TTFrame and runs the program.
 */
@SuppressWarnings("serial")
public class TTTFrame extends JFrame {

	/**
	 * Here the global variable are declared.
	 * 
	 * Three JPanels are used to organize components within the frame
	 * A JTextArea stores sample text and a JTextPane for user text 
	 * RESULTS_HEADER stores the "Begin typing..." prompt at the top of the results panel
	 * RESULTS_TABLE is used to display the WPM, current time, and word count
	 * RESET_BUTTON is for resetting the test
	 * TIMER for updating RESULTS_TABLE
	 * TIME_START stores System time at which the user starts the test
	 * TARTED is used to determine when to start the TIMER
	 * WORD_COUNT represents words typed by user, and ERROR_COUNT the number of error characters
	 */
	private JPanel CONTENT_PANE, TEST_TEXT_PANEL, TYPING_FIELD_PANEL, RESULTS_PANEL;
	private JTextPane TYPING_FIELD;
	private JTextArea TEST_TEXT_AREA;
	private JLabel RESULTS_HEADER;
	private JTable RESULTS_TABLE;
	private JButton RESET_BUTTON;
	private Timer TIMER;
	private long TIME_START;
	private boolean STARTED;
	private int WORD_COUNT, ERROR_COUNT;
	private String SAMPLE_TEXT;

	/**
	 * Constructor for the frame; creates intial content pane and
	 * calls methods to create and arrange the various JPanel components.
	 */
	public TTTFrame() 
	{
		setIconImage(Toolkit.getDefaultToolkit().getImage("C:\\Users\\Justin\\Pictures\\Reference\\Vitera.png"));
		setResizable(false);
		setTitle("Vitera Timed Typing Test");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(100, 100, 570, 350);
				
		CONTENT_PANE = new JPanel();
		CONTENT_PANE.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(CONTENT_PANE);
		CONTENT_PANE.setLayout(null);
		
		testTextPanelSetup();
		typingFieldPanelSetup();
		resultsSetup();
	}
	
	/**
	 * testTextSetup creates the JPanel for the sample text area and adds it to the frame.
	 * This is the method where dynamic sample text would be generated/read into the program in future iterations.	
	 */
	private void testTextPanelSetup()
	{
		//This is where the sample text the user must type can be changed.  Right now its hard coded, but we could read
		//it in from a file or prompt as well if we wanted.
		SAMPLE_TEXT = "Hello; this text can only be changed in the code right now, but if we wanted, we could update that code to read a file " +
				"or document and get the text from there.  Just something to think about while you take the test.  Enjoy, and good luck.  " +
				"My record was 72wpm with 1% error.  How well can you do?";
		
		TEST_TEXT_PANEL = new JPanel();
		TEST_TEXT_PANEL.setBounds(6, 6, 250, 155);
		CONTENT_PANE.add(TEST_TEXT_PANEL);
		TEST_TEXT_PANEL.setLayout(new BorderLayout(0, 0));
		
		TEST_TEXT_AREA = new JTextArea(SAMPLE_TEXT);
		TEST_TEXT_AREA.setBackground(new Color(211, 211, 211));
		TEST_TEXT_AREA.setWrapStyleWord(true);
		TEST_TEXT_AREA.setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null));
		TEST_TEXT_AREA.setLineWrap(true);
		TEST_TEXT_AREA.setEditable(false);
		TEST_TEXT_PANEL.add(TEST_TEXT_AREA, BorderLayout.CENTER);
	}
	
	/**
	 * typingFieldSetup creates the panel for user text to be entered, and creates the TTTDocListener
	 * to monitor the a user's input and update the results table accordingly.
	 */
	private void typingFieldPanelSetup()
	{
		TYPING_FIELD_PANEL = new JPanel();
		TYPING_FIELD_PANEL.setBounds(6, 173, 250, 133);
		CONTENT_PANE.add(TYPING_FIELD_PANEL);
		TYPING_FIELD_PANEL.setLayout(new BorderLayout(0, 0));
		
		TYPING_FIELD = new JTextPane();
		TYPING_FIELD.setToolTipText("Type here to take the Timed Typing Test");
		TYPING_FIELD.setBorder(new BevelBorder(BevelBorder.LOWERED, null, null, null, null));
		TYPING_FIELD.getDocument().addDocumentListener(new TTTDocListener());
		TYPING_FIELD_PANEL.add(TYPING_FIELD, BorderLayout.CENTER);
	}
	
	/**
	 * resultsSetup creates and organizes the results table, header, and reset button.
	 */
	private void resultsSetup()
	{		
		RESULTS_PANEL = new JPanel();
		RESULTS_PANEL.setBounds(268, 6, 280, 300);
		CONTENT_PANE.add(RESULTS_PANEL);
		RESULTS_PANEL.setLayout(new BorderLayout(0, 0));
		
		RESULTS_TABLE = new JTable(8,2);
		RESULTS_TABLE.setFont(new Font("SansSerif", Font.BOLD, 14));
		RESULTS_TABLE.setEnabled(false);
		RESULTS_TABLE.setShowGrid(false);
		RESULTS_TABLE.setRowSelectionAllowed(false);
		RESULTS_TABLE.setBackground(new Color(211, 211, 211));
		RESULTS_PANEL.add(RESULTS_TABLE, BorderLayout.CENTER);
		//These three lines right-align the results data.
		DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer();
		rightRenderer.setHorizontalAlignment( JLabel.RIGHT );
		RESULTS_TABLE.getColumnModel().getColumn(1).setCellRenderer( rightRenderer );
		
		//Row headings that will be left aligned.  WPM isn't displayed until the end of the test.
		RESULTS_TABLE.setValueAt("Total Time:", 0, 0);
		RESULTS_TABLE.setValueAt("Words Typed:",1,0);
		RESULTS_TABLE.setValueAt("WPM:",4,0);
		RESULTS_TABLE.setValueAt("Error Percentage:",6,0);
				
		RESET_BUTTON = new JButton("PRESS TO RESET");
		/*
		 * The reset button calls reset() to perform the necessary actions, but only if
		 * STARTED is true, which would imply that a test has begun or ended.  If reset were called
		 * prior to the start of a test, errors would occur.
		 */
		RESET_BUTTON.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) 
			{
				if (STARTED)
					reset();
			}
		});
		RESULTS_PANEL.add(RESET_BUTTON, BorderLayout.SOUTH);
		
		//This JLabel's text could be changed if required.  For now, it statically displays the start prompt.
		RESULTS_HEADER = new JLabel("Start typing sample text to begin test");
		RESULTS_HEADER.setForeground(new Color(102, 0, 0));
		RESULTS_HEADER.setFont(new Font("SansSerif", Font.ITALIC, 13));
		RESULTS_PANEL.add(RESULTS_HEADER, BorderLayout.NORTH);
	}
	
	/**
	 * This is the DocumentListener implementation class for TTTFrame.
	 * Whenever text in the TYPING_FIELD changes, methods are called to update the results table,
	 * to update the timer, and to update the word count.  The TIMER is also defined in the constructor here.
	 */
	class TTTDocListener implements DocumentListener
	{
		public TTTDocListener()
		{
			TIMER = new Timer(1000, new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
				{
					updateResults(1);
				}
			});
		}		
		
		/**
		 * Stub for DocumentListener method. 
		 */
		@Override
		public void changedUpdate(DocumentEvent e){}

		/**
		 * Called whenever text is entered in the testing area, via typing or pasting clipboard
		 * text.  Starts TIMER if STARTED is false, and stops TIMER when end-conditions are met.
		 * Also updates results table at intervals, and makes the testing area uneditable when end
		 * conditions are met.
		 */
		@Override
		public void insertUpdate(DocumentEvent e) 
		{
			/*
			 * If STARTED is true, this is not the first character and nothing is done.
			 * If STARTED is false, it is the first character the user has typed and the TIMER
			 * is started while the current System time is recorded for later use.
			*/
			if(!STARTED)
			{
				STARTED = true;
				TIME_START = System.currentTimeMillis();
				TIMER.start();
			}
			
			try {	setWordCount(e.getDocument().getText(0,e.getDocument().getLength()));	} 
			catch (BadLocationException e1) {	e1.printStackTrace();	}
			
			updateResults(2);
			
			/*
			 * The end-condition for the test is whether or not the user text is equal in length
			 * to the sample text.  If it is, the testing text area is set to uneditable and the timer is stopper.
			 * WPM is also calculated and written to the results table.
			 */
		    if(e.getDocument().getLength() >= TEST_TEXT_AREA.getText().length())
		    	endTest();
		}

		/**
		 * Called whenever a user deletes somethign from the testing area.  Updates
		 * word count if a word has been deleted.  Doesn't worry about test-ending conditions
		 * because user could never go over the character limit by deletion.
		 */
		@Override
		public void removeUpdate(DocumentEvent e)
		{
			try {	setWordCount(e.getDocument().getText(0,e.getDocument().getLength()));	} 
			catch (BadLocationException e1) {	e1.printStackTrace();	}
			
			updateResults(2);
		}		
	}
	
	/**
	 * setWordCount is given the current contents of the user text field in the form of a string,
	 * and updates the global word count variable.
	 */
	private void setWordCount(String S)
	{
		//The global word count is set to zero so that backspacing does not generate a new word.
		WORD_COUNT = 0;
		//chunk starts out true to avoid a space at the start of the test being counted as a word.
		boolean chunk = true;
		int word_count = 0;
		
		for(int i = 0; i<S.length(); i++)
		{
			/*
			 * chunk is used to ignore simultaneous whitespace and punctuation when calculating word count,
			 * like the double space one might find following a period.
			 * Here, if a character is not a letter, and we are not in a chunk, we know that a new
			 * word has been finished.
			 */
			if(!Character.isLetter(S.charAt(i)) && !chunk)
			{
				chunk = true;
				word_count++;
			}
			
			/*
			 * If a character is a letter, it is either the end of a chunk or the middle
			 * of a current word.  Either way, chunk is false.
			 */
			else if(Character.isLetter(S.charAt(i)))
				chunk = false;
		}
		
		//Global WORD_COUNT is updated to reflect the newly calculated word count
		WORD_COUNT = word_count;
	}
	
	
	
	/**
	 * endTest is called when the closing conditions for the typing test are met (i.e. user text length equals
	 * test text length).  End updates the results table with final values, stops TIMER, and makes the text
	 * field uneditable.  It also calculates the WPM value for the results table.
	 */
	private void endTest()
	{
    	TYPING_FIELD.setEditable(false);
    	errorCheck();
    	
    	//WPM = word count * 60 / total time in seconds
		updateResults(3);
		//Error Percentage = error count / test text length * 100
		updateResults(4);
    	TIMER.stop();    	
	}
	
	/**
	 * updateResults is used to update the results table without accessing it every time from the individual
	 * methods.  It requires an integer code to update; this is NOT IDEAL, but this project is being done in
	 * a JRE6 environment and switch only works with integers.  In an update to JRE7, code would be a String
	 * and would correspond directly to the row being updated (i.e. "WPM", "TIME" vs 3, 1).
	 */
	private void updateResults(int code)
	{
		DecimalFormat ndnn = new DecimalFormat("#.##");
		switch(code)
		{
			//case 1 is a TIME update
			case 1: RESULTS_TABLE.setValueAt((System.currentTimeMillis()-TIME_START)/1000,0,1); break;
			//case 2 is a WORDS update
			case 2: RESULTS_TABLE.setValueAt(WORD_COUNT,1,1); break;
			//case 3 is a WPM update	
			case 3: RESULTS_TABLE.setValueAt((WORD_COUNT * 60)/((System.currentTimeMillis() - TIME_START)/1000),4,1); break;
			//case 4 updates the error percentage
			case 4: double d = ((double)ERROR_COUNT/TEST_TEXT_AREA.getDocument().getLength()*100);
					RESULTS_TABLE.setValueAt(ndnn.format(d) + "%",6,1); break;
			//case 5 empties the results data
			case 5: RESULTS_TABLE.setValueAt("",0,1); RESULTS_TABLE.setValueAt("",1,1); 
					RESULTS_TABLE.setValueAt("",4,1); RESULTS_TABLE.setValueAt("",6,1); break;
			default: 
		}
	}
	
	/**
	 * errorCheck compares the user text to the sample text and calls methods to highlight errors
	 * and push the error percentage to the result table.
	 */
	private void errorCheck()
	{
		String sample_text = null, user_text = null;
		ERROR_COUNT = 0;
		
		try 
		{
			sample_text = TEST_TEXT_AREA.getDocument().getText(0, TEST_TEXT_AREA.getDocument().getLength());
			user_text = TYPING_FIELD.getDocument().getText(0, TYPING_FIELD.getDocument().getLength());
		} 
		catch (BadLocationException e) {	e.printStackTrace();	}
		
		for(int i = 0; i<sample_text.length(); i++)
		{
			if(sample_text.charAt(i) != user_text.charAt(i))
			{
				ERROR_COUNT++;
				highlightErrors(i);
			}
		}
		
		System.out.println("errors:" + ERROR_COUNT);
		System.out.println("text:" + TEST_TEXT_AREA.getDocument().getLength());
		System.out.println("percentage:" + ((double)ERROR_COUNT/(double)TEST_TEXT_AREA.getDocument().getLength())*100);
	}
	
	/**
	 * highlightErrors highlights differences between user and test text one character at a time
	 */
	private void highlightErrors(int i)
	{
		DefaultHighlighter.DefaultHighlightPainter highlighter = new DefaultHighlighter.DefaultHighlightPainter(Color.RED);
		try {	TYPING_FIELD.getHighlighter().addHighlight(i, i+1, highlighter);	}
		catch (BadLocationException e) {	e.printStackTrace();	}
	}
	
	/**
	 * reset empties the results table, clears the typing field, and resets the necessary global variables
	 * to their starting states so that a new test can begin.  TIMER is stopped in case users hit reset in the
	 * middle of an unfinished test.
	 */
	private void reset()
	{
		WORD_COUNT = 0;
		STARTED = false;
		TYPING_FIELD.setEditable(true);
		
		try {	TYPING_FIELD.getDocument().remove(0,TYPING_FIELD.getDocument().getLength());	}
		catch (BadLocationException e1) {	e1.printStackTrace();	}
		
		updateResults(5);
		
		TIMER.stop();
	}
	
	/**
	 * main method; creates a TTTFrame inside a new runnable.
	 */
	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				try{	TTTFrame frame = new TTTFrame();	frame.setVisible(true);		}
				catch (Exception e){	e.printStackTrace();	}
			}
		});
	}
}